const state = {	domMap: {} };
const actionsMap = {
	sid: 0,
	sCounter: 0,
	seqIdKey: function() {
		let self = this;
		if(0 == self.sid){
			self.sid = new Date().getTime();
		}		
		return '_s_n_i_d_' + self.sid;
	
	},
	
	increaseSeq: function(){
		let self = this;
		if(0 == self.sid){
			self.sid = new Date().getTime();
		}
		self.sCounter = self.sCounter + 1;
		return self.sid + self.sCounter;
	
	},
	
	nowSeqCounter: function(){
		return this.sCounter;
	
	},

	buildNodesMap: function(){
		let self = this;
		state.domMap = self.traverseNodes([document]);
		//console.log(state.domMap);

	},

	refreshNodesMap: function(updates){
		let self = this;
		let nodeUpdates = updates.nodeUpdates;
		if(nodeUpdates){
			//console.log('before nodes update: ' + Object.keys(state.domMap).length);
			let domMap = state.domMap;
			let added = null == nodeUpdates.added ? {} : nodeUpdates.added;
			let removed = null == nodeUpdates.removed ? {} : nodeUpdates.removed;
			for(let key in added){
				domMap[key] = added[key];
			}
			for(let key in removed){
				delete domMap[key];
			}
			//console.log('after nodes update: ' + Object.keys(state.domMap).length);
		}

	},

	buildNodesSnapshot: function(nodesMap){
		//console.log('start build nodes snapshot: ' + Object.keys(nodesMap).length);
		//console.log(nodesMap);
		let self = this;
		let snapshotMap = {};
		let idKey = self.seqIdKey();
		for(let key in nodesMap){
			let nodeInfo = nodesMap[key];
			let nodeRef = nodeInfo.node;
			if(null == nodeRef){
				console.log('node not exists: ' + key);
				continue;
			}
			let nodeSnapshot = self.getNodeSnapshot(idKey, nodeRef)
			if(null == nodeSnapshot.nodeName || '' == nodeSnapshot.nodeName){
				console.log('empty node name');
				console.log(nodeRef);
				continue;
			}
			snapshotMap[key] = nodeSnapshot;
		}
		//console.log('build nodes snapshot finish: ' + Object.keys(snapshotMap).length);
		return snapshotMap;
		
	},

	getNodeSnapshot: function(idKey, nodeRef){
		let parentRef = nodeRef.parentNode;
		let nextSiblingRef = nodeRef.nextSibling;
		let previousSiblingRef = nodeRef.previousSibling;
		let nodeSnapshot = {
			id: nodeRef[idKey],
			pid: null != parentRef ? parentRef[idKey] : 0,
			psid: null != previousSiblingRef ? previousSiblingRef[idKey] : 0,
			nsid: null != nextSiblingRef ? nextSiblingRef[idKey] : 0,
			nodeType: nodeRef.nodeType,
			nodeName: nodeRef.nodeName,
			nodeValue: nodeRef.nodeValue,
			
		}
		return nodeSnapshot;

	},

	traverseNodes: function(nodes){
		//console.log('start traverse notes, origins: ' + nodes.length);
		let self = this;
		let nodesMap = {};
		let seqIdKey = self.seqIdKey();
		while(nodes.length > 0){
			let nodeRef = nodes.pop();
			let nodeName = nodeRef.nodeName;
			let refInfo = {
				node: nodeRef, 
				refSeqId: nodeRef[seqIdKey]
			};
			if(null == refInfo.refSeqId){
				refInfo.refSeqId = self.increaseSeq();
				nodeRef[seqIdKey] = refInfo.refSeqId;
			}
			if(null == nodesMap[refInfo.refSeqId]){
				nodesMap[refInfo.refSeqId] = refInfo;
			}
			if('iframe' == nodeName.toLowerCase()){
				let iframeDoc = nodeRef.contentDocument;
				//console.log(nodeRef);
				//console.log(iframeDoc);
				if(null != iframeDoc){
					nodes.unshift(iframeDoc);
				}
			}
			let childNodes = nodeRef.childNodes;
			for(let i = 0; i < childNodes.length; i++){
				nodes.unshift(childNodes[i]);
			}
		}
		//console.log('nodes traverse finish: ' + Object.keys(nodesMap).length);
		return nodesMap;

	},

	handleNodeHighlight: function(node, clear){
		if(!node.getBoundingClientRect){
			return;		
		}

		let maskId = 'crawler-mask';
		let existsMask = document.getElementById(maskId);
		if(clear){
			if(null != existsMask){
				existsMask.remove();
			}
			return;
		}
		
		let parentNode = node.parentNode;
		if('chrome-ext' == node.__from__ || 'chrome-ext' == parentNode.__from__){
			return;
		}

		let coordinate = node.getBoundingClientRect();
		let mask = document.createElement('div');
		mask.__from__ = 'chrome-ext';
		mask.id = maskId;
		mask.classList.add('mask');

		let topLine = document.createElement('div');
		topLine.__from__ = 'chrome-ext';
		topLine.classList.add('line');
		topLine.classList.add('horizontal');
		topLine.classList.add('top');
		topLine.style.top = coordinate.top + 'px';

		let leftLine = document.createElement('div');
		leftLine.__from__ = 'chrome-ext';
		leftLine.classList.add('line');
		leftLine.classList.add('vertical');
		leftLine.classList.add('left');
		leftLine.style.left = coordinate.left + 'px';

		let bottomLine = document.createElement('div');
		bottomLine.__from__ = 'chrome-ext';
		bottomLine.classList.add('line');
		bottomLine.classList.add('horizontal');
		bottomLine.classList.add('bottom');
		bottomLine.style.top = coordinate.bottom + 'px';
		
		let rightLine = document.createElement('div');
		rightLine.__from__ = 'chrome-ext';
		rightLine.classList.add('line');
		rightLine.classList.add('vertical');
		rightLine.classList.add('right');
		rightLine.style.left = coordinate.right + 'px';

		mask.appendChild(topLine);
		mask.appendChild(leftLine);
		mask.appendChild(bottomLine);
		mask.appendChild(rightLine);
	
		let fragment = new DocumentFragment();

		fragment.appendChild(mask);
		if(null != existsMask){
			existsMask.remove();
		}

		let root = document.getElementById('crawler');
		if(null != root){
			root.appendChild(fragment);
		}
	
	},
	

	handleMutations: function(mutations, observer){
		let self = this;
		// console.log('receive mutaions: ');
		// console.log(mutations);
		let mutationsMap = {node: [], attr: [], text: []};
		for(let i = 0; i < mutations.length; i++){
			let mutation = mutations[i];
			if('childList' == mutation.type){
				mutationsMap.node.push(mutation);
				continue;
			}
			// if('attributes' == mutation.type){
			// 	mutationsMap.attr.push(mutation);
			// 	continue;
			// }
			// if('characterData' == mutation.type){
			// 	mutationsMap.text.push(mutation);
			// 	continue;
			// }
		}

		let domMap = {};
		let snapshotMap = {};
		if(mutationsMap.node.length > 0){
			let nodeUpdates = self.handelNodeListMutation(mutationsMap.node);
			domMap.nodeUpdates = nodeUpdates.dom;
			snapshotMap.nodeUpdates = nodeUpdates.snapshot;
		}
		// if(mutationsMap.attr.length > 0){
		// 	let attrUpdates = self.handleAttributeListMutation(mutationsMap.attr);
		// 	domMap.attrUpdates = attrUpdates;
		// }
		// if(mutationsMap.text.length > 0){
		// 	let textUpdates = self.handleCharacterListMutation(mutationsMap.text);
		// 	domMap.textUpdates = textUpdates;
		// }
		
		self.refreshNodesMap(domMap);

		let updateMsg = {type: 'event', data: {name: 'nodeSnapshotUpdated', data: snapshotMap}};
		backPortKeeper.sendMessage(updateMsg);

	},

	handelNodeListMutation: function(mutations){
		//console.log('handle node mutations');
		//console.log(mutations);
		let self = this;
		let deltaMap = {added: [], removed: []};
		for(let i = 0; i < mutations.length; i++){
			let mutation = mutations[i];
			let list = mutation.addedNodes;
			if(list.length > 0){
				self.list2Array(list).forEach(e => deltaMap.added.push(e));
			}
			list = mutation.removedNodes;
			if(list.length > 0){
				self.list2Array(list).forEach(e => deltaMap.removed.push(e));
			}
		}
		let addedMap = self.handleMutationNode(deltaMap.added);
		let removedMap = self.handleMutationNode(deltaMap.removed);
		let nodeUpdates = {
			dom: {
				added: addedMap.dom, 
				removed: removedMap.dom
			},
			snapshot: {
				added: addedMap.snapshot, 
				removed: removedMap.snapshot
			}
		};
		return nodeUpdates;

	},

	handleMutationNode: function(nodes){
		let self = this;
		let tmpNode = null;
		let deltaNodes = [];
		for(let i = 0; i < nodes.length; i++){
			tmpNode = nodes[i];
			if(null != tmpNode.id && 'crawler' == tmpNode.id){
				console.log('skip mutation node handle for ');
				console.log(tmpNode);
				continue;
			}
			deltaNodes.push(tmpNode);
		}
		let domMap = self.traverseNodes(deltaNodes);
		let nodesSnapshot = self.buildNodesSnapshot(domMap);
		return {dom: domMap, snapshot: nodesSnapshot};

	},

	handleAttributeListMutation: function(mutations){
		console.log('handle attr mutations');
		console.log(mutations);

	},

	handleCharacterListMutation: function(mutations){
		console.log('handle text mutations');
		console.log(mutations);

	},

	list2Array: function(list){
		let array = [];
		for(let i = 0; i < list.length; i++){
			array[i] = list[i];
		}
		return array;

	},

};

const crawlerActions = {
	state: null,
	enableCrawler: function(){
		let self = this;
		
		self.disableCrawler();

		let crawlerApp = document.createElement('div');
		crawlerApp.id = 'crawler';
		crawlerApp.__from__ = 'chrome-ext';
		
		let crawlerConsole = document.createElement('iframe');
		crawlerConsole.id = 'crawler-console';
		crawlerConsole.src = chrome.runtime.getURL('crawler-app/console/index.html');
		crawlerConsole.__from__ = 'chrome-ext';

		let crawlerContext = document.createElement('iframe');
		crawlerContext.id = 'crawler-context';
		crawlerContext.src = chrome.runtime.getURL('crawler-app/context/index.html');
		crawlerContext.__from__ = 'chrome-ext';

		crawlerApp.appendChild(crawlerConsole);
		crawlerApp.appendChild(crawlerContext);
		
		document.body.appendChild(crawlerApp);

		self.registerDomObserver();
		self.registerMouseListener();

		self.state = 'enabled';

	},

	disableCrawler: function(){
		let self = this;
		let crawler = document.getElementById('crawler');
		if(null != crawler){
			crawler.remove();
		}
		self.releaseDomObserver();
		self.releaseMouseListener();
		self.state = 'disabled';

	},
	
	exitCrawler: function(){
		let self = this;
		let crawler = document.getElementById('crawler');
		if(null != crawler){
			crawler.remove();
		}
		self.releaseDomObserver();
		self.releaseMouseListener();
		self.state = 'disabled';

	},

	listeners: {
		onMouseMove: function(event){crawlerActions.handleMouseEvent('mousemove', event)},
		onMouseClick: function(event){crawlerActions.handleMouseEvent('mouseclick', event)},
		onContextMenu: function(event){crawlerActions.handleMouseEvent('contextmenu', event)},
	
	},

	registerMouseListener: function(){
		let self = this;
		self.releaseMouseListener();
		window.addEventListener('click', self.listeners.onMouseClick, true);
		window.addEventListener('mousemove', self.listeners.onMouseMove, true);
		window.addEventListener('contextmenu', self.listeners.onContextMenu, false);

	},

	releaseMouseListener: function(){
		let self = this;
		window.removeEventListener('click', self.listeners.onMouseClick, true);
		window.removeEventListener('mousemove', self.listeners.onMouseMove, true);
		window.removeEventListener('contextmenu', self.listeners.onContextMenu, false);
	
	},

	handelActionClick: function(actionMsg){
		console.log(actionMsg);
		let self = this;
		if(null == self.state || 'disabled' == self.state){
			crawlerActions.enableCrawler();
			backPortKeeper.sendMessage({type: 'cmd', data: {name: 'action', data: {type: 'enable', color: 'red'}}});
			return;
		}
		crawlerActions.disableCrawler();
		backPortKeeper.sendMessage({type: 'cmd', data: {name: 'action', data: {type: 'disable'}}});

	},

	handleMouseEvent: function(type, event){
		//console.log(event);
		let self = this;
		let target = event.target;
		if('mousemove' == type && null != target){
			actionsMap.handleNodeHighlight(target, false);
			return;
		}
		
		if('mouseclick' == type && null != target){
			event.preventDefault();
			let crawlerContext = document.getElementById('crawler-context');
			crawlerContext.style.display = 'none';
			return;
		}

		if('contextmenu' == type){
			event.stopPropagation();
			event.preventDefault();
			let crawlerContext = document.getElementById('crawler-context');
			crawlerContext.style.top = event.y + 'px';
			crawlerContext.style.left = event.x + 'px';
			crawlerContext.style.display = 'block';
			return;
		}

	},
	observer: null,
	observeConfig: {
		subtree: true,
		childList: true,
		attributes: true,
		attributeOldValue: true,
		characterData: true,
		characterDataOldValue: true
	},

	registerDomObserver: function(){
		let self = this;
		if(null == self.observer){
			self.observer = new MutationObserver(function(mutations, observer){
				actionsMap.handleMutations(mutations, observer);
			});
		}
		
		self.observer.observe(document, self.observeConfig);

	},

	releaseDomObserver: function() {
		let self = this;
		if(null != self.observer){
			self.observer.disconnect();
		}

	}

};

const contextActions = {
	state: null,
	handelActionMsg: function(msgInfo){
		console.log(msgInfo);
		let self = this;
		let menuMsg = msgInfo.data.data;
		let name = menuMsg.type;
		let contextMenu = menuMsg.data;
		
		if('click' == name && 'addCralwerData' == contextMenu.name){
			return;
		}

		if('click' == name && 'exitCrawler' == contextMenu.name){
			crawlerActions.handelActionClick({type: 'disable', from: 'context'});
		}

	}

};

const consoleActions = {
	state: null,
	handelActionMsg: function(actionMsg){
		console.log(actionMsg);
		let self = this;

	}

};

const backPortKeeper = {
	mid: 0,
	mConter: 0,	
	tid: null,
	port : null,
	portName: 'content:' + new Date().getTime(),
	connected: false,
	isReconnect: false,
	register: function(){
		let self = this;
		self.port = chrome.runtime.connect(null, {name: self.portName});
		self.port.onMessage.addListener(function(msg){
			//console.log('receive backend message : ' + JSON.stringify({sid: msg.sid, type: msg.type, name: msg.data.name}));
			self.handlePortMsg(msg);
		
		});
		
		self.port.onDisconnect.addListener(function(){
			console.log('back connection lost');
			self.port = null;
			self.connected = false;
			self.isReconnect = true;
			self.register();

		});

		self.connected = true;
		if(!self.isReconnect){
			self.sendMessage({type: 'event', data: {name: 'ping'}}, self.port);
		}

	},

	startKeeper: function(){
		let self = this;
		actionsMap.buildNodesMap();
		self.register();

	},

	handlePortMsg: function(msg){
		let self = this;
		let type = msg.type;
		let name = msg.data.name;
		if('event' == type && 'pong' == name){
			self.tabId = msg.data.data;
			return;
		}
		if('event' == type && 'menuClick' == name){
			contextActions.handleMenuClick(msg);
			return;
		}
		if('event' == type && 'actionClick' === name){
			crawlerActions.handelActionClick(msg);
			return;
		}
		if('cmd' == type){
			if('buildNodesSnapshot' == name){
				let domMap = state.domMap;
				let nodesSnapshot = actionsMap.buildNodesSnapshot(domMap);
				let respMsg = {type: 'cmd-resp', data: {name: msg.data.name, data: nodesSnapshot}};
				self.sendMessage(respMsg);
				return;
			}
			if('addHighlight' == name || 'clearHighlight' == name){
				let domMap = state.domMap;
				let domRef = domMap[msg.data.data];
				if(null != domRef){
					actionsMap.handleNodeHighlight(domRef.node, 'clearHighlight' == name);
				}
				return;
			}
			if('cleaup' == name){
				return;
			}
		}

	},
	
	sendMessage: function(msgInfo){
		let self = this;
		if(0 == self.mid){
			self.mid = new Date().getTime();
		}
		self.mConter = self.mConter + 1;
		msgInfo.tid = self.tabId;
		msgInfo.sid = 'content:' + self.mid + self.mConter;
		msgInfo.from = 'content';
		msgInfo.sender = self.portName;
		if(!self.connected){
			//console.log('backend connection lost, discard msg: ' + JSON.stringify(msgInfo));
			return;
		}
		self.port.postMessage(msgInfo);
	
	}

};

const winPortKeeper = {
	selfPort: null,
	contextPort: null,
	consolePort: null,
	startKeeper: function(){
		let self = this;
		self.selfPort = window;
		self.selfPort.addEventListener('message', function(msgEvt){self.handleWinPortMsg(msgEvt)}, true);

	},

	handleWinPortMsg: function(msgEvt){
		let self = this;
		//console.log('receive app message');
		//console.log(msgEvt);
		let msg = msgEvt.data;
		let from = null == msg ? null : msg.from;
		if(null == msg || null == from 
			|| ('context' != from && 'console' != from)){
			return;
		}
		let type = msg.type;
		let name = msg.data.name;
		if('event' == type && 'ready' == name){
			if('context' == from){
				self.contextPort = document.getElementById('crawler-context').contentWindow;
			}
			if('console' == from){
				self.consolePort = document.getElementById('crawler-console').contentWindow;
			}
			return;
		}
		if('event' == type && 'context' == from){
			contextActions.handelActionMsg(msg);
			return;
		}
		if('event' == type && 'console' == from){
			consoleActions.handelActionMsg(msg);
			return;
		}	
	},

	mid: 0,
	mCounter: 0,
	sendPortMsg: function(msgInfo, portType){
		//console.log('send message to app');
		//console.log(msgInfo);
		let self = this;
		if(0 == self.mid){
			self.mid = new Date().getTime();
		}
		let destPort = 'context' == portType ? self.contextPort : self.consolePort;
		self.mCounter = self.mCounter + 1;
		msgInfo.sid = 'content:' + self.mid + self.mCounter;
		msgInfo.from = 'content';
		destPort.postMessage(msgInfo, "*");

	},

};

backPortKeeper.startKeeper();
winPortKeeper.startKeeper();